package com.wujunshen.dumpling.database.mysql.query;

import com.wujunshen.dumpling.common.utils.Assert;
import com.wujunshen.dumpling.common.utils.JdbcUtils;
import com.wujunshen.dumpling.database.metadata.IColumn;
import com.wujunshen.dumpling.database.metadata.IDatabase;
import com.wujunshen.dumpling.database.metadata.IPrimaryKey;
import com.wujunshen.dumpling.database.mysql.mapping.Mapping;
import com.wujunshen.dumpling.database.mysql.model.MySqlColumn;
import com.wujunshen.dumpling.database.mysql.model.MySqlDatabase;
import com.wujunshen.dumpling.database.mysql.model.MySqlPrimaryKey;
import com.wujunshen.dumpling.database.mysql.model.MySqlTable;
import com.wujunshen.dumpling.database.query.DataBaseQuery;
import com.wujunshen.dumpling.database.query.DataBaseType;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import static com.wujunshen.dumpling.common.constant.Constants.*;

/**
 * mysql 数据库查询
 *
 * <p>jdbc url带有 useInformationSchema=true,才会查询数据库表INFORMATION_SCHEMA
 *
 * @author frankwoo
 */
@Slf4j
@NoArgsConstructor
public class MySqlDataBaseQuery implements DataBaseQuery {
  /** 缓存 */
  private final Map<String, List<IColumn>> columnsCaching = new ConcurrentHashMap<>();

  private final Map<String, List<IColumn>> allColumnsCaching = new ConcurrentHashMap<>();

  /** DataSource */
  @Getter @Setter public DataSource dataSource;

  /** The Connection. */
  protected Connection connection;

  @Override
  public DataBaseType getDataBaseType() {
    return DataBaseType.MYSQL;
  }

  /**
   * 获取连接对象，单例模式，采用双重锁检查
   *
   * @return {@link Connection}
   */
  @SneakyThrows
  private Connection getConnection() {
    // 不为空
    if (!Objects.isNull(connection) && !connection.isClosed()) {
      return connection;
    }
    // 同步代码块
    synchronized (DataBaseQuery.class) {
      // 为空或者已经关闭
      if (Objects.isNull(connection) || connection.isClosed()) {
        this.connection = this.getDataSource().getConnection();
      }
    }

    return Optional.of(this.connection).orElse(null);
  }

  /**
   * 获取 getCatalog
   *
   * @return {@link String}
   */
  @SneakyThrows
  protected String getCatalog() {
    return Optional.of(Objects.requireNonNull(this.getConnection()).getCatalog()).orElse(null);
  }

  /**
   * 获取 getSchema
   *
   * @return {@link String}
   */
  @SneakyThrows
  protected String getSchema() {
    return Objects.requireNonNull(this.getConnection()).getSchema();
  }

  /**
   * 获取 DatabaseMetaData
   *
   * @return {@link DatabaseMetaData}
   */
  @SneakyThrows
  protected DatabaseMetaData getMetaData() {
    return Optional.of(Objects.requireNonNull(this.getConnection()).getMetaData()).orElse(null);
  }

  /**
   * Prepare statement prepared statement.
   *
   * @param sql {@link String} SQL
   * @param values {@link java.util.Arrays} 参数值
   * @return {@link PreparedStatement}
   */
  @SneakyThrows
  protected PreparedStatement prepareStatement(String sql, String... values) {
    Assert.isNotEmpty(sql);

    PreparedStatement preparedStatement =
        Objects.requireNonNull(this.getConnection()).prepareStatement(sql);

    if (ArrayUtils.isEmpty(values)) {
      return preparedStatement;
    }

    for (int i = 1; i < values.length + 1; i++) {
      preparedStatement.setString(i, values[i - 1]);
    }

    return preparedStatement;
  }
  /**
   * 获取数据库
   *
   * @return {@link IDatabase} 数据库信息
   */
  @SneakyThrows
  @Override
  public IDatabase getDataBase() {
    MySqlDatabase mySqlDatabase = new MySqlDatabase();
    // 当前数据库名称
    mySqlDatabase.setDataBase(getCatalog());
    return mySqlDatabase;
  }

  /**
   * 获取表信息
   *
   * @return {@link List} 所有表信息
   */
  @SneakyThrows
  @Override
  public List<MySqlTable> getTables() {
    // 查询
    ResultSet resultSet =
        getMetaData().getTables(getCatalog(), getSchema(), PERCENT, new String[] {"TABLE"});

    // 映射
    List<MySqlTable> mySqlTableList = Mapping.convertList(resultSet, MySqlTable.class);

    JdbcUtils.close(resultSet);

    return mySqlTableList;
  }

  /**
   * 获取列信息
   *
   * @param table {@link String} 表名
   * @return {@link List} 表字段信息
   */
  @SneakyThrows
  @Override
  public List<? extends IColumn> getTableColumns(String table) {
    Assert.isNotEmpty(table);

    if (table.equals(PERCENT)) {
      return getAllColumns();
    }

    // 查询
    ResultSet resultSet = getMetaData().getColumns(getCatalog(), getSchema(), table, PERCENT);
    // 映射
    List<MySqlColumn> resultList = Mapping.convertList(resultSet, MySqlColumn.class);

    List<String> tableNameList =
        resultList.stream().map(MySqlColumn::getTableName).distinct().collect(Collectors.toList());

    if (columnsCaching.isEmpty()) {
      // 单表查询
      resultSet =
          prepareStatement(
                  GET_COLUMNS_SQL + "  and TABLE_NAME = ?", getDataBase().getDataBase(), table)
              .executeQuery();

      // 处理列，表名为key，列名为值
      List<MySqlColumn> finalResultList = Mapping.convertList(resultSet, MySqlColumn.class);
      tableNameList.forEach(
          tableName ->
              columnsCaching.put(
                  tableName,
                  finalResultList.stream()
                      .filter(mySqlColumn -> mySqlColumn.getTableName().equals(tableName))
                      .collect(Collectors.toList())));
    }

    handleColumnInfo(resultList, columnsCaching);

    JdbcUtils.close(resultSet);

    return resultList;
  }

  /**
   * 获取所有列信息
   *
   * @return {@link List} 表字段信息
   */
  @SneakyThrows
  @Override
  public List<? extends IColumn> getAllColumns() {
    // 查询
    ResultSet resultSet = getMetaData().getColumns(getCatalog(), getSchema(), PERCENT, PERCENT);
    // 映射
    List<MySqlColumn> resultList = Mapping.convertList(resultSet, MySqlColumn.class);

    List<String> tableNameList =
        resultList.stream().map(MySqlColumn::getTableName).distinct().collect(Collectors.toList());

    if (allColumnsCaching.isEmpty()) {
      // 查询全部
      // 获取表列信息SQL 查询表名、列名、说明、数据类型
      resultSet = prepareStatement(GET_COLUMNS_SQL, getDataBase().getDataBase()).executeQuery();
      if (resultSet.getFetchSize() < FETCH_SIZE) {
        resultSet.setFetchSize(FETCH_SIZE);
      }

      // 处理列，表名为key，列名为值
      List<MySqlColumn> finalResultList = Mapping.convertList(resultSet, MySqlColumn.class);
      tableNameList.forEach(
          tableName ->
              allColumnsCaching.put(
                  tableName,
                  finalResultList.stream()
                      .filter(mySqlColumn -> mySqlColumn.getTableName().equals(tableName))
                      .collect(Collectors.toList())));
    }

    handleColumnInfo(resultList, allColumnsCaching);

    JdbcUtils.close(resultSet);

    return resultList;
  }

  /**
   * 两个缓存互相独立处理数据，不会串<br>
   * 否则同一个缓存，执行完单表查询后，再执行全表查询，缓存中数据是不正确的，这是一个bug<br>
   *
   * @param resultList {@link List} 表字段信息
   * @param cache {@link Map} 缓存
   */
  private void handleColumnInfo(List<MySqlColumn> resultList, Map<String, List<IColumn>> cache) {
    // 处理备注信息
    resultList.forEach(
        mySqlColumn -> {
          // 从缓存中根据表名获取列信息
          List<IColumn> columnsCachingList = cache.get(mySqlColumn.getTableName());

          columnsCachingList.stream()
              .filter(
                  // 列名表名一致
                  iColumn ->
                      mySqlColumn.getColumnName().equals(iColumn.getColumnName())
                          && mySqlColumn.getTableName().equals(iColumn.getTableName()))
              .forEach(
                  iColumn -> {
                    // 放入列类型
                    mySqlColumn.setColumnType(iColumn.getColumnType());
                    mySqlColumn.setColumnLength(iColumn.getColumnLength());
                  });
        });
  }

  /**
   * 根据表名获取主键
   *
   * @param table {@link String}
   * @return {@link List}
   */
  @SneakyThrows
  @Override
  public List<? extends IPrimaryKey> getPrimaryKeys(String table) {
    // 查询
    ResultSet resultSet = getMetaData().getPrimaryKeys(getCatalog(), getSchema(), table);
    // 映射
    List<? extends IPrimaryKey> result = Mapping.convertList(resultSet, MySqlPrimaryKey.class);

    JdbcUtils.close(resultSet, this.connection);

    return result;
  }

  /**
   * 根据表名获取主键信息
   *
   * @return {@link List}
   */
  @SneakyThrows
  @Override
  public List<? extends IPrimaryKey> getPrimaryKeys() {
    ResultSet resultSet =
        prepareStatement(GET_PRIMARYS_SQL, getDataBase().getDataBase()).executeQuery();

    List<? extends IPrimaryKey> result = Mapping.convertList(resultSet, MySqlPrimaryKey.class);

    JdbcUtils.close(resultSet);

    return result;
  }
}
